package com.jdd.fintech.morse.plugin.ecrypts;

import com.jdd.fintech.morse.plugin.constans.OpenApiConstans;
import com.jdd.fintech.morse.plugin.enums.EncrAndDecrAlgorithmTypeEnum;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.util.Assert;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.lang.reflect.Array;
import java.security.Provider;
import java.security.spec.AlgorithmParameterSpec;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 对称加密算法<br>
 * 在对称加密算法中，数据发信方将明文（原始数据）和加密密钥一起经过特殊加密算法处理后，使其变成复杂的加密密文发送出去。<br>
 * 收信方收到密文后，若想解读原文，则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密，才能使其恢复成可读明文。<br>
 * 在对称加密算法中，使用的密钥只有一个，发收信双方都使用这个密钥对数据进行加密和解密，这就要求解密方事先必须知道加密密钥。<br>
 *
 * 目前系统默认使用24位密钥加密,当设置的密钥不足24位时会用0在后不足,查过24位时只截取24位
 */
public class SymmetricCrypto implements Serializable {
    private static final long serialVersionUID = 1L;

    public final static String ENCODING = "UTF-8";

    /**
     * SecretKey 负责保存对称密钥
     */
    private SecretKey secretKey;
    /**
     * 密钥长度
     */
    private int secretKeyLength;
    /**
     * Cipher负责完成加密或解密工作
     */
    private Cipher cipher;
    /**
     * 加密解密参数
     */
    private AlgorithmParameterSpec params;
    /**
     * 是否0填充
     */
    private boolean isZeroPadding;
    private final Lock lock = new ReentrantLock();

    /**
     * 构造
     *
     * @param algorithm 算法 {@link EncrAndDecrAlgorithmTypeEnum}
     * @param key       自定义KEY
     * @since 3.1.2
     */
    public SymmetricCrypto(EncrAndDecrAlgorithmTypeEnum algorithm, String key) {
        this(algorithm, null, key, OpenApiConstans.TRIPLE_DES_SECRET_KEY_LENGTH, null);
    }

    /**
     * 构造
     *
     * @param algorithm  算法
     * @param cipher     加密模式
     * @param key        密钥
     * @param keyLength  密钥长度
     * @param paramsSpec 算法参数，例如加盐等
     * @since 3.3.0
     */
    public SymmetricCrypto(EncrAndDecrAlgorithmTypeEnum algorithm, String cipher, String key, Integer keyLength, AlgorithmParameterSpec paramsSpec) {
        if(keyLength != null) {
            this.secretKeyLength = keyLength;
        }else {
            this.secretKeyLength = OpenApiConstans.AES_SECRET_KEY_LENGTH;
        }
        init(algorithm, cipher, key);
        if (null != paramsSpec) {
            setParams(paramsSpec);
        }
    }

    /**
     * 初始化
     *
     * @param algorithm 算法
     * @param key       密钥
     * @return {@link SymmetricCrypto}的子对象，即子对象自身
     */
    public SymmetricCrypto init(EncrAndDecrAlgorithmTypeEnum algorithm, String cipher, String key) {
        Assert.notNull(algorithm, "'algorithm' must be not blank !");
        Assert.notNull(key, "'key' must be not blank !");

        this.secretKey = generateSecretKey(algorithm, key);

        try {
            String cipher0 = StringUtils.defaultString(cipher, algorithm.getDefaultCipher());
            if (algorithm == EncrAndDecrAlgorithmTypeEnum.SM4) {
                Provider provider = new BouncyCastleProvider();
                this.cipher = Cipher.getInstance(cipher0, provider);
            } else {
                this.cipher = Cipher.getInstance(cipher0);
            }
        } catch (Exception e) {
            throw new RuntimeException("初始化加密算法失败", e);
        }
        return this;
    }

    /**
     * 设置 {@link AlgorithmParameterSpec}，通常用于加盐或偏移向量
     *
     * @param params {@link AlgorithmParameterSpec}
     * @return 自身
     */
    public SymmetricCrypto setParams(AlgorithmParameterSpec params) {
        this.params = params;
        return this;
    }

    /**
     * 设置偏移向量
     *
     * @param iv {@link IvParameterSpec}偏移向量
     * @return 自身
     */
    public SymmetricCrypto setIv(IvParameterSpec iv) {
        setParams(iv);
        return this;
    }

    /**
     * 设置偏移向量
     *
     * @param iv 偏移向量，加盐
     * @return 自身
     */
    public SymmetricCrypto setIv(byte[] iv) {
        setIv(new IvParameterSpec(iv));
        return this;
    }

    // --------------------------------------------------------------------------------- Encrypt

    /**
     * 加密
     *
     * @param data 被加密的bytes
     * @return 加密后的bytes
     */
    public byte[] encrypt(byte[] data) {
        lock.lock();
        try {
            if (null == this.params) {
                cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            } else {
                cipher.init(Cipher.ENCRYPT_MODE, secretKey, params);
            }
            return cipher.doFinal(paddingDataWithZero(data, cipher.getBlockSize()));
        } catch (Exception e) {
            throw new RuntimeException("加密异常", e);
        } finally {
            lock.unlock();
        }
    }

    /**
     * 加密，使用UTF-8编码
     *
     * @param data 被加密的字符串
     * @return 加密后的Hex
     */
    public String encryptHex(String data) {
        try {
            return Hex.encodeHexString(encrypt(data.getBytes(ENCODING)));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("加密异常", e);
        }
    }

    /**
     * 加密，使用UTF-8编码
     *
     * @param data 被加密的字符串
     * @return 加密后的Base64
     */
    public String encryptBase64(String data) {
        try {
            return new BASE64Encoder().encode(encrypt(data.getBytes(ENCODING)));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("加密失败,错误的编码类型", e);
        }
    }


    // --------------------------------------------------------------------------------- Decrypt

    /**
     * 解密
     *
     * @param bytes 被解密的bytes
     * @return 解密后的bytes
     */
    public byte[] decrypt(byte[] bytes) {
        final int blockSize;
        final byte[] decryptData;

        lock.lock();
        try {
            if (null == this.params) {
                cipher.init(Cipher.DECRYPT_MODE, secretKey);
            } else {
                cipher.init(Cipher.DECRYPT_MODE, secretKey, params);
            }
            blockSize = cipher.getBlockSize();
            decryptData = cipher.doFinal(bytes);
        } catch (Exception e) {
            throw new RuntimeException("解密异常", e);
        } finally {
            lock.unlock();
        }

        return removePadding(decryptData, blockSize);
    }

    /**
     * 解密, 使用UTF-8编码
     *
     * @param data 被加密的字符串
     * @return 加密后的Base64
     */
    public String decryptBase64(String data) {
        try {
            byte[] decryptByte = new BASE64Decoder().decodeBuffer(data);
            return new String(decrypt(decryptByte), ENCODING);
        } catch (Exception e) {
            throw new RuntimeException("解密失败,错误的编码类型", e);
        }
    }

    /**
     * 处理文件下载时候的编码问题
     * @param encryptedData
     * @return
     * @throws Exception
     */
    public String decryptForFile(String encryptedData) throws Exception {
        try {
            byte[] decryptByte = new BASE64Decoder().decodeBuffer(encryptedData);
            return Base64.encodeBase64String(decrypt(decryptByte));
        } catch (Exception e) {
            throw new RuntimeException("解密失败,错误的编码类型", e);
        }
    }

    /**
     * 解密，使用UTF-8编码
     *
     * @param data 被加密的字符串
     * @return 解密后的原始数据
     */
    public String decryptHex(String data) {
        try {
            byte[]  decryptByte = Hex.decodeHex(data.toCharArray());
            return new String(decrypt(decryptByte), ENCODING);
        } catch (UnsupportedEncodingException | DecoderException e) {
            throw new RuntimeException("解密异常", e);
        }
    }


    /**
     * 加密文件
     * @param sourceFile
     * @param diminationFile
     * @throws Exception
     */
    public void encrypt(String sourceFile, String diminationFile) throws Exception {
        InputStream is = null;
        OutputStream out = null;
        CipherInputStream cis = null;
        try {
            is = new FileInputStream(sourceFile);
            out = new FileOutputStream(diminationFile);

            this.cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            cis = new CipherInputStream(is, this.cipher);
            byte[] buffer = new byte[1024];
            int r;
            while ((r = cis.read(buffer)) > -1) {
                out.write(buffer, 0, r);
            }
            out.flush();
        } finally {
            if (null != cis) {
                cis.close();
            }
            if (null != out) {
                out.close();
            }

            if (null != is) {
                is.close();
            }
        }
    }

    /**
     * 解密文件
     * @param sourceFileName
     * @param diminationFileName
     * @throws Exception
     */
    public void decrypt(String sourceFileName, String diminationFileName) throws Exception {
        InputStream is = null;
        OutputStream out = null;
        CipherOutputStream cos = null;
        try {
            is = new FileInputStream(sourceFileName);
            out = new FileOutputStream(diminationFileName);

//            DESedeKeySpec spec = new DESedeKeySpec(keyVal.getBytes("utf-8"));
//            SecretKeyFactory keyfactory = SecretKeyFactory.getInstance(ALGORITHM);
//            Key deskey = keyfactory.generateSecret(spec);
//            Cipher decryptCipher = Cipher.getInstance(MODE);
            this.cipher.init(Cipher.DECRYPT_MODE, secretKey);

            cos = new CipherOutputStream(out, this.cipher);
            byte[] buffer = new byte[1024];
            int r;
            while ((r = is.read(buffer)) >= 0) {
                cos.write(buffer, 0, r);
            }
            cos.flush();
        } finally {
            if (null != cos) {
                cos.close();
            }
            if (null != out) {
                out.close();
            }

            if (null != is) {
                is.close();
            }
        }
    }


    // --------------------------------------------------------------------------------- Getters

    /**
     * 获得对称密钥
     *
     * @return 获得对称密钥
     */
    public SecretKey getSecretKey() {
        return secretKey;
    }

    /**
     * 获得加密或解密器
     *
     * @return 加密或解密
     */
    public Cipher getCipher() {
        return cipher;
    }

    /**
     * 数据按照blockSize的整数倍长度填充填充0
     *
     * <p>
     * 在ZeroPadding模式下，且数据长度不是blockSize的整数倍才有效，否则返回原数据
     *
     * <p>
     * 见：https://blog.csdn.net/OrangeJack/article/details/82913804
     *
     * @param data      数据
     * @param blockSize 块大小
     * @return 填充后的数据，如果isZeroPadding为false或长度刚好，返回原数据
     * @since 4.6.7
     */
    protected byte[] paddingDataWithZero(byte[] data, int blockSize) {
        if (this.isZeroPadding) {
            final int length = data.length;
            // 按照块拆分后的数据中多余的数据
            final int remainLength = length % blockSize;
            if (remainLength > 0) {
                // 新长度为blockSize的整数倍，多余部分填充0
                return resize(data, length + blockSize - remainLength);
            }
        }
        return data;
    }

    /**
     * 数据按照blockSize去除填充部分，用于解密
     *
     * <p>
     * 在ZeroPadding模式下，且数据长度不是blockSize的整数倍才有效，否则返回原数据
     *
     * @param data      数据
     * @param blockSize 块大小
     * @return 去除填充后的数据，如果isZeroPadding为false或长度刚好，返回原数据
     * @since 4.6.7
     */
    protected byte[] removePadding(byte[] data, int blockSize) {
        if (this.isZeroPadding) {
            final int length = data.length;
            final int remainLength = length % blockSize;
            if (remainLength == 0) {
                // 解码后的数据正好是块大小的整数倍，说明可能存在补0的情况，去掉末尾所有的0
                int i = length - 1;
                while (i >= 0 && 0 == data[i]) {
                    i--;
                }
                return resize(data, i + 1);
            }
        }
        return data;
    }

    /**
     * 生成一个新的重新设置大小的数组<br>
     * 调整大小后拷贝原数组到新数组下。扩大则占位前N个位置，其它位置补充0，缩小则截断
     *
     * @param array   原数组
     * @param newSize 新的数组大小
     * @return 调整后的新数组
     * @since 4.6.7
     */
    protected static byte[] resize(byte[] array, int newSize) {
        if (newSize < 0) {
            return array;
        }
        if (null == array) {
            return null;
        }
        final int length = Array.getLength(array);
        final byte[] newArray = new byte[newSize];
        if (newSize > 0 && length > 0) {
            //noinspection SuspiciousSystemArraycopy
            System.arraycopy(array, 0, newArray, 0, Math.min(length, newSize));
        }
        return newArray;
    }

    /**
     *  获取加密key
     *
     * @param algorithm
     * @param secretKey
     * @return
     */
    protected SecretKey generateSecretKey(EncrAndDecrAlgorithmTypeEnum algorithm, String secretKey){
        byte[] sk = resize(secretKey.getBytes(), this.secretKeyLength);
        return new SecretKeySpec(sk,  algorithm.getValue());
    }



}
